// Copyright (c) Microsoft Corporation.
// 
// Author: Vance Morrison
// 

using System;
using EnvDTE;
using EnvDTE80;
using System.Text;
using Microsoft.VisualStudio.CommandBars;
using System.IO;
using System.Diagnostics;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using VSAddins;
using System.Threading;

// See code:#Overview for more.
/// <summary>
/// #Overview The HyperAddin.dll is a Visual Studio addin that provides some funtionality that is generally
/// useful mostly around encouraging code to be easy to read and navigate.
/// 
/// code:HyperAddin uses a generic addin class code:VSInterface which provides a simplified interface to
/// Visual Studio and makes adding commands significantly easier. The commands that HyperAddin provides are
/// defined in the code:HyperAddin.Initialize method.
/// 
/// Interesting functionality include 
///    * code:HyperAddin.GoOutCommand - the Go out command
///    * code:HyperAddin.HyperLinkAction - What happens when a hyperlink is clicked.  
/// 
/// code:HyperAddin also uses the code:HyperLink to implement hyperlink interception and code:CodeBrowser to
/// then navigate to particular pieces of code.
/// </summary>
public class HyperAddin : VSInterface
{
    /// <summary>
    /// Initialize is the main routine that needs to be overriden to define the specific interface that
    /// HyperAddin provides.  
    /// </summary>
    public override void Initialize(DTE2 dte, bool UIInit)
    {
        Log.WriteLine("Initializing"); 
        CommandInfo command;
        // Initialize the objects that actually do the work. 
        wordWrap = new WordWrap(dte);           // Enable on the fly word-wrapping 
#if Development
            addinDevelopment = new AddinDevelopment(dte);
#endif

        // enable the hyperlinking in comments, 
        if (!UIInit && hyperLink == null)
        {
            Log.WriteLine("Registering hyperlinks");
            hyperLink = new HyperLink(dte);
            try
            {
                if (hyperLink.RegisterHyperLinkPrefix("code"))
                    Log.WriteLine(@"Updated the HKCR\Protocol\Handers to include the code: prefix");
            }
            catch (Exception e)
            {
                Log.WriteLineAlways("Error: Failure while registering 'code:' prefix (hyperlinks won't work).");
                Log.WriteLineAlways("   Exception: " + e.Message);
                if (e is System.Security.SecurityException)
                    Log.WriteLineAlways("On VISTA, you must run Visual Studio at least once with elevated privileges.");
            }
            hyperLink.OnHyperLinkClick += this.HyperLinkAction;
        }

        AddinName = "HyperAddin";

        // #NavagationCommands Initialization
        command = new CommandInfo("GoOut", new ActionNoArg(this.GoOutCommand));
        command.KeyboardBinding = "Global::Ctrl+,";
        command.OnToolBar = true;
        AddCommand(command);

        // #CommentCommands
        command = new CommandInfo("Formating.FormatComment", new ActionNoArg(wordWrap.FormatCommentCommand));
        command.KeyboardBinding = "Global::Alt+C,Alt+F";
        command.OnContextMenu = true;
        command.OnToolBar = true;
        AddCommand(command);

        command = new CommandInfo("Formatting.DisableWordWrap", new ActionNoArg(wordWrap.DisableWordWrapCommand));
        command.Enabled = delegate { return wordWrap.Enabled; };
        AddCommand(command);

        command = new CommandInfo("Formatting.SetWordWrap", new ActionNoArg(wordWrap.SetWordWrapCommand));
        AddCommand(command);

        // Add the user commands.
        HyperAddinCommand[] userCommands = HyperAddinSettings.Default.UserCommands;
        if (userCommands != null)
        {
            foreach (HyperAddinCommand userCommand in userCommands)
            {
                if (userCommand.Name != null && userCommand.CommandLine != null)
                {
                    command = new CommandInfo(userCommand.Name, MakeUserCommand(userCommand));
                    if (userCommand.ShortCut != null)
                    {
                        Log.WriteLine("Adding global Shortcut " + userCommand.ShortCut + " for user command " + userCommand.Name);
                        command.KeyboardBinding = "Global::" + userCommand.ShortCut;
                    }
                    AddCommand(command);
                    Log.WriteLine("Added user command " + userCommand.Name + " CommandLine: " + userCommand.CommandLine);
                }
            }
        }

        // #MiscCommands
        command = new CommandInfo("DisableLogging", delegate
        {
            Log.WriteLine("Disabling logging");
            HyperAddinSettings.Default.Logging = Log.Enabled = false;
            HyperAddinSettings.Default.Save();
        });
        command.Enabled = delegate { return Log.Enabled; };
        AddCommand(command);

        command = new CommandInfo("EnableLogging", delegate
        {
            HyperAddinSettings.Default.Logging = Log.Enabled = true;
            Log.WriteLine("Enabing logging");
            HyperAddinSettings.Default.Save();
        });
        command.Enabled = delegate { return !Log.Enabled; };
        AddCommand(command);

        command = new CommandInfo("CancelUserCommand", delegate
        {
            if (runningCommand != null)
            {
                Log.WriteLineAlways("**** Canceling command " + runningCommand.CommandLine);
                canceled = true;
                runningCommand.Kill();
            }
        });
        command.Enabled = delegate { return runningCommand != null; };
        command.KeyboardBinding = "Global::Alt+C,Alt+C";
        AddCommand(command);

        command = new CommandInfo(AddinName + "Help", new ActionNoArg(this.HelpCommand));
        command.OnToolBar = true;
        AddCommand(command);
#if Development 
            command = new CommandInfo("Addin.PrintStructure", new ActionNoArg(addinDevelopment.PrintDiagnosticsCommand));
            AddCommand(command);
#endif

    }
    public override void Dispose()
    {
        if (wordWrap != null)
            wordWrap.Dispose();
    }

    //Commands directly handled by the HyperAdding Class 

    /// <summary>
    /// Navigate to the next outer scope (method, class, file).  
    /// </summary>
    public void GoOutCommand()
    {
        CodeBrowser.GoOut(DTE);
        TryToShowAndExpandNear(DTE);
    }

    #region private
    /// <summary>
    /// This routine is called every time a user CTRL-Clicks on a hyperlink in Visual Studio. 
    /// </summary>
    private void HyperLinkAction(object obj, HyperLinkEventArgs args)
    {
        DTE2 dte = DTE;
        string hyperLinkText = args.Text;

        Log.WriteLine("\r\nLooking up code hyperlink: " + hyperLinkText);
        dte.StatusBar.Text = "Looking up " + hyperLinkText + "...";
        dte.StatusBar.Animate(true, vsStatusAnimation.vsStatusAnimationFind);

        // Dispatch the hyperlink
        string message = "Hyperlinked to " + hyperLinkText;
        try
        {
            Match m = Regex.Match(hyperLinkText, @"^(\w+)\:(.*?)(#(\w+))?$");
            if (m.Success)
            {
                string hyperLinkKind = m.Groups[1].Value;
                string symbol = m.Groups[2].Value;
                string anchor = m.Groups[4].Value;

                if (hyperLinkKind == "code")
                {
                    args.Handled = true;
                    if (symbol.Length > 0)
                        CodeBrowser.GoToSymbolInSelectionScope(dte, symbol);
                }
                else if (hyperLinkKind == "file")
                {
                    // Fix relative file URLs so that they are relative to the current source
                    // Relative URLS start with a . or a letter (but not something that looks like a drive
                    // eg C:
                    m = Regex.Match(symbol, @"(^[\.])|(^\w$)|(^\w[^:].*$)");
                    if (m.Success)
                    {
                        args.Handled = true;
                        string relativePath = symbol.Replace('/', '\\');
                        relativePath = relativePath.Replace("%20", " ");        // allow escaping spaces
                        string curDocPath = dte.ActiveWindow.Document.FullName;
                        string fullPath = curDocPath;
                        if (relativePath.Length != 0)
                            fullPath = Path.Combine(Path.GetDirectoryName(curDocPath), relativePath);

                        if (File.Exists(fullPath))
                            dte.ExecuteCommand("File.OpenFile", "\"" + fullPath + "\"");
                        else
                            throw new ApplicationException("Could not find file: " + fullPath);
                    }
                    else
                        Log.WriteLine("File URL is not relative, allowing default processing.");
                }
                else
                    Log.WriteLine("Not a code: or file: URL, allowing default processing.");

                if (args.Handled && anchor.Length > 0)
                    GoToAnchorInActiveDoc(dte, anchor);
            }
            else
                Log.WriteLine("Hyperlink did not match URL pattern:" + hyperLinkText);

            if (args.Handled)
                TryToShowAndExpandNear(dte);
        }
        catch (Exception e)
        {
            message = e.Message;
            System.Media.SystemSounds.Beep.Play();
        }

        if (args.Handled)
        {
            Log.WriteLine(message);
            dte.StatusBar.Text = message;
            dte.StatusBar.Animate(false, vsStatusAnimation.vsStatusAnimationFind);
        }
        else
        {
            Log.WriteLine("Doing default URL processing for " + hyperLinkText);
        }
        Log.Flush();
    }
    /// <summary>
    /// Given an anchor string, find a line in the active file that starts with whitespace # and then
    /// the anchor (but allowing C# // comments and VB ' comments).  
    /// </summary>
    private static void GoToAnchorInActiveDoc(DTE2 dte, string anchor)
    {
        Log.WriteLine("Going to anchor #" + anchor);
        TextDocument doc = (TextDocument)dte.ActiveDocument.Object("TextDocument");

        string anchorPattern = @"^[ \t'/#]*\#" + anchor;
        EditPoint searchPoint = doc.StartPoint.CreateEditPoint();
        EditPoint endAnchor = searchPoint.CreateEditPoint();
        TextRanges tags = null;
        bool success = false;
        try
        {
            success = searchPoint.FindPattern(anchorPattern, (int)
                (vsFindOptions.vsFindOptionsMatchInHiddenText | vsFindOptions.vsFindOptionsRegularExpression),
                ref endAnchor, ref tags);
        }
        catch { }
        if (!success)
            throw new ApplicationException("Could not find anchor " + anchor + " in " + doc.Parent.FullName);

        // Highlight the anchor that we found.  
        TextSelection selection = doc.Selection;
        selection.MoveToPoint(searchPoint, false);
        // Only select the anchor itself, not the context 
        string matchingText = searchPoint.GetText(endAnchor);
        selection.CharRight(false, matchingText.IndexOf('#'));
        selection.MoveToPoint(endAnchor, true);

        Log.WriteLine("Goto anchor succeeded: " + anchor);
    }
    /// <summary>
    /// Like 'TryToShow, but also expands the first outlined thing that is within a few (5) 
    /// lines before the selection.  
    /// </summary>
    private static void TryToShowAndExpandNear(DTE2 dte)
    {
        TextDocument txt = (TextDocument)dte.ActiveDocument.Object("TextDocument");
        TextSelection selection = txt.Selection;
        EditPoint anchorPoint = selection.AnchorPoint.CreateEditPoint();
        EditPoint origPoint = selection.ActivePoint.CreateEditPoint();
        int origLine = origPoint.Line;
        selection.StartOfLine(vsStartOfLineOptions.vsStartOfLineOptionsFirstColumn, false);

        selection.GotoLine(origLine + 1, false);       // Force expansion if we are on a collapsed node. 
        selection.GotoLine(origLine, false);

        /// Also force expansion of some nodes up to 6 lines back. 
        for (int i = 1; i < 6; i++)
        {
            selection.LineUp(false, 1);
            int curLine = selection.ActivePoint.Line;
            if (curLine <= 1)
                break;

            if (curLine + i != origLine)            // Found an outlined thing.  
            {
                selection.GotoLine(curLine + 1, false);       // Force expansion
                selection.GotoLine(curLine, false);
                break;
            }
        }
        selection.ActivePoint.TryToShow(vsPaneShowHow.vsPaneShowCentered, origPoint);
        selection.MoveToPoint(anchorPoint, false);        // Put the selection back.  
        selection.MoveToPoint(origPoint, true);
    }

    private ActionNoArg MakeUserCommand(HyperAddinCommand userCommand)
    {
        return delegate() { RunUserCommand(userCommand); };
    }

    #region RunUserCommand
    private Microsoft.Scripting.Command runningCommand;     // Currently run command
    private bool canceled;                                  // Did we cancel the command (for better user messages)

    private void RunUserCommand(HyperAddinCommand userCommand)
    {
        try
        {
            if (runningCommand != null)
            {
                DTE.StatusBar.Text = "Command already running, use HyperAddin->CancelUserCommand to cancel";
                System.Media.SystemSounds.Beep.Play();
                return;
            }

            // Save all files (unless the current doc is readonly (we assume the command is check-out)
            // TODO this is a hack. 
            bool saveAll = true;
            if (DTE.ActiveDocument != null)
            {
                string activeDoc = DTE.ActiveDocument.FullName;
                if (!string.IsNullOrEmpty(activeDoc) &&
                    File.Exists(activeDoc) &&
                    (File.GetAttributes(activeDoc) & FileAttributes.ReadOnly) != 0)
                    saveAll = false;
            }
            if (saveAll)
                DTE.ExecuteCommand("File.SaveAll", "");

            Log.WriteLineAlways("\r\n**** Running Command " + userCommand.Name + " CommandLine: " + userCommand.CommandLine);
            DTE.StatusBar.Text = "Running User command " + userCommand.Name + " ...";
            DTE.StatusBar.Animate(true, vsStatusAnimation.vsStatusAnimationGeneral);

            canceled = false;
            string commandLine = SubstituteVariables(userCommand.CommandLine);
            Log.WriteLineAlways("**** Executing " + commandLine);
            runningCommand = new Microsoft.Scripting.Command(commandLine,
                new Microsoft.Scripting.CommandOptions().AddTimeout(Timeout.Infinite).AddNoThrow());

            // Wait for the responce.  
            int outputIndex = 0;
            int tickCount = 0;
            System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();
            timer.Interval = 100;   // 100 msec = .1sec
            timer.Enabled = true;
            timer.Tick += delegate
            {
                // This code is run every 100 msec.  
                tickCount++;
                bool exited = runningCommand.HasExited;

                // Flush output. 
                if (runningCommand.OutputLength > outputIndex)
                {
                    string output = runningCommand.Output;
                    string toOutput = output.Substring(outputIndex);
                    Log.WriteAlways(toOutput);
                    outputIndex = output.Length;
                }

                if (exited)
                {
                    if (canceled)
                    {
                        Log.WriteLineAlways("**** User command canceled");
                        DTE.StatusBar.Text = "User command canceled: " + commandLine;
                    }
                    else
                    {
                        Log.WriteLineAlways("**** Command exited with value " + runningCommand.ExitCode);
                        if (runningCommand.ExitCode == 0)
                            DTE.StatusBar.Text = "User command complete: " + commandLine;
                        else
                            DTE.StatusBar.Text = "User command failed: " + commandLine;
                    }
                    DTE.StatusBar.Animate(false, vsStatusAnimation.vsStatusAnimationGeneral);
                    timer.Enabled = false;        // Turn off the timer (break the loop)
                    runningCommand = null;        // indicate that another command can run
                    System.Media.SystemSounds.Beep.Play();
                }
                else if (tickCount % 100 == 0)
                {
                    int seconds = tickCount / 10;
                    DTE.StatusBar.Text = "Command running for " + seconds + " secs, use HyperAddin->CancelUserCommand to cancel.";
                }
            };  // End of delegate body
        }
        catch (Exception e)
        {
            string message = e.Message;
            if (!(e is ApplicationException))
                message = e.ToString();
            Log.WriteLineAlways("**** Exception Thrown while processing command " + userCommand.Name);
            Log.WriteLineAlways("    " + message);
            DTE.StatusBar.Text = "User command failed: " + userCommand.Name;
            System.Media.SystemSounds.Beep.Play();
        }
        Log.FlushAlways();
    }

    /// <summary>
    /// Returns a string where various $(var) string are replaced with their value
    /// </summary>
    private string SubstituteVariables(string command)
    {
        command = Regex.Replace(command, @"\$\((\w+)\)", delegate(Match m)
        {
            string varName = m.Groups[1].Value;
            string varValue = "";
            if (varName == "SolutionDir" && DTE.Solution != null && !string.IsNullOrEmpty(DTE.Solution.FileName))
                varValue = Path.GetDirectoryName(DTE.Solution.FileName);
            else if (varName == "SolutionPath" && DTE.Solution != null && !string.IsNullOrEmpty(DTE.Solution.FileName))
                varValue = DTE.Solution.FileName;
            else if (varName == "ActiveDocument" && DTE.ActiveDocument != null && !string.IsNullOrEmpty(DTE.ActiveDocument.FullName))
                varValue = DTE.ActiveDocument.FullName;
            else
                throw new ApplicationException("Unknown variable " + varName + " in command " + command);
            return "\"" + varValue + "\"";
        });
        return command;
    }
    #endregion

    private WordWrap wordWrap;
    private HyperLink hyperLink;
#if Development
        private AddinDevelopment addinDevelopment;
#endif
    #endregion
}

/// <summary>
/// Implements word wrapping functionality in comments.  
/// 
/// The most interesting routines are code:WordWrap.BeforeKeyPress and code:WordWrap.FormatCommentCommand
/// </summary>
class WordWrap : IDisposable
{
    /// <summary>
    /// WordWrap is responsible for two features
    ///     * Itercepting newlines and space characters and causing word wrapping 'on the fly'
    ///     * Providing the 'FormatComment' command which will do word wrapping on a block that is
    ///         messed up.  
    /// </summary>
    public WordWrap(DTE2 dte)
    {
        this.dte = dte;
        wrapColumn = HyperAddinSettings.Default.WordWrapColumn;

        EnvDTE80.Events2 events = (EnvDTE80.Events2)dte.Events;
        textDocKeyEvents = (EnvDTE80.TextDocumentKeyPressEvents)events.get_TextDocumentKeyPressEvents(null);
        textDocKeyEvents.BeforeKeyPress += new _dispTextDocumentKeyPressEvents_BeforeKeyPressEventHandler(BeforeKeyPress);
    }

    public void Dispose()
    {
        if (textDocKeyEvents != null)
            textDocKeyEvents.BeforeKeyPress -= new _dispTextDocumentKeyPressEvents_BeforeKeyPressEventHandler(BeforeKeyPress);
    }

    /// <summary> 
    /// Pops a dialog to get the wrap column and enables word wrap
    /// </summary>
    public void SetWordWrapCommand()
    {
        if (wrapColumn < 0)
            wrapColumn = -wrapColumn;

        string response = Microsoft.VisualBasic.Interaction.InputBox("Word Wrap Column", "Word Wrap", wrapColumn.ToString(), 0, 0);
        int responceInt;
        if (String.IsNullOrEmpty(response))    // was the dialog box canceled.   
            return;

        if (Int32.TryParse(response, out responceInt) && responceInt > 0)
        {
            HyperAddinSettings.Default.WordWrapColumn = wrapColumn = responceInt;
            HyperAddinSettings.Default.Save();
        }
        else
            Microsoft.VisualBasic.Interaction.MsgBox("Invalid input number", Microsoft.VisualBasic.MsgBoxStyle.Exclamation, null);
    }

    // Command Implementations

    /// <summary>
    /// Disable the word wrapping functionality
    /// </summary>
    public void DisableWordWrapCommand()
    {
        if (wrapColumn > 0)
            HyperAddinSettings.Default.WordWrapColumn = wrapColumn = -wrapColumn;  // negative is disabled, but remembers value
        HyperAddinSettings.Default.Save();
    }

    /// <summary>
    /// This command uses the selection point to find a region to word wrap.  
    /// If there is a user selection then that is extened to complete lines 
    /// and that is used.  Otherwise the comment block associated with the
    /// current cursor position is used.  
    /// 
    /// See FormatCommentString for details on how the word wrapping works. 
    /// </summary>
    public void FormatCommentCommand()
    {
        if (wrapColumn <= 0)
            return;
        Log.WriteLine("Formatting comment wrapColumn = " + wrapColumn);
        TextSelection selection = (TextSelection)dte.ActiveDocument.Selection;

        // Get the start end end points (round down and up to the start of a line). 
        EditPoint startPoint = selection.AnchorPoint.CreateEditPoint();
        startPoint.StartOfLine();

        EditPoint endPoint = selection.ActivePoint.CreateEditPoint();
        endPoint.StartOfLine();
        endPoint.LineDown(1);
        Log.WriteLine("Formatting comment from lines " + startPoint.Line + " to " + endPoint.Line);

        // If the selection is empty, find the extent of the comment block the
        // selection point is in and use that. 
        if (selection.IsEmpty)
        {
            // Find the start of the block
            while (!startPoint.AtStartOfDocument)
            {
                if (!IsCommentLine(startPoint))
                {
                    startPoint.LineDown(1);
                    break;
                }
                startPoint.LineUp(1);
            }

            // Find the end of the block
            while (!endPoint.AtEndOfDocument)
            {
                if (!IsCommentLine(endPoint))
                    break;
                endPoint.LineDown(1);
            }
            Log.WriteLine("Empty selection found comment block at " + startPoint.Line + " to " + endPoint.Line);
        }

        // Do the morph.  
        string comment = startPoint.GetText(endPoint);
        string newComment = FormatCommentString(comment);
        startPoint.ReplaceText(endPoint, newComment,
            (int)(vsEPReplaceTextOptions.vsEPReplaceTextNormalizeNewlines | vsEPReplaceTextOptions.vsEPReplaceTextTabsSpaces));
    }

    /// <summary>
    /// Returns true if the word wrapping is on.  
    /// </summary>
    public bool Enabled { get { return wrapColumn > 0; } }

    #region private
    /// <summary>
    /// Called before any keyboard key press in the editor.  Can be used to do all sorts of magic,
    /// Here we use it to implement word wrapping
    /// </summary>
    private void BeforeKeyPress(string Keypress, EnvDTE.TextSelection Selection, bool InStatementCompletion, ref bool CancelKeypress)
    {
        if (wrapColumn <= 0)        // This means disable it.  
            return;

        if (Keypress == "\r" || Keypress == "\t" || (Keypress == " " && Selection.CurrentColumn >= wrapColumn))
        {
            VirtualPoint curPos = Selection.ActivePoint;
            EditPoint ep = curPos.CreateEditPoint();
            ep.StartOfLine();
            string lineText = ep.GetText(curPos);
            int commentCharsIdx = SkipSpace(lineText, 0);

            string commentStart = "//";
            if (dte.ActiveDocument.FullName.EndsWith(".vb", StringComparison.OrdinalIgnoreCase))
                commentStart = "'";

            // Are we in a comment
            if (String.Compare(lineText, commentCharsIdx, commentStart, 0, commentStart.Length) == 0)
            {
                int commentStartIdx = commentCharsIdx + commentStart.Length;
                if (commentStartIdx < lineText.Length && lineText[commentStartIdx] == commentStart[0])  // allow /// syntax
                    commentStartIdx++;

                if (Keypress == "\t")
                {
                    // Don't interfere with intellisense of the XML tags.  If we do a tab when we see
                    // something that looks like a XML tag, don't process it specially but let it through.  
                    if (Regex.IsMatch(lineText, @"</?\w+[^>]*$"))
                        return;

                    CancelKeypress = true;
                    // if comments begin with a '// ' the tab start after the space. 
                    if (commentStartIdx < lineText.Length && lineText[commentStartIdx] == ' ')
                        commentStartIdx++;

                    int curColumn0 = lineText.Length - commentStartIdx;        // zero based column    
                    int numSpaces = 4 - (curColumn0 % 4);
                    Selection.Insert(new String(' ', numSpaces), (int)vsInsertFlags.vsInsertFlagsCollapseToEnd);
                }
                else
                {
                    CancelKeypress = true;
                    int endIdx = SkipSpace(lineText, commentStartIdx);
                    string prefix = lineText.Substring(0, endIdx);

                    if (Keypress == " ")
                    {
                        if (endIdx + 2 <= lineText.Length && lineText[endIdx] == '*' && lineText[endIdx + 1] == ' ')
                            prefix += "    ";

                        // Find the previous space and insert the text there. 
                        int lastSpace = lineText.LastIndexOf(' ');
                        if (lastSpace > 0)
                        {
                            ep.CharRight(lastSpace);
                            string textToInsert = "\r\n" + prefix + lineText.Substring(lastSpace + 1) + " ";
                            ep.ReplaceText(Selection.ActivePoint, textToInsert, (int)vsEPReplaceTextOptions.vsEPReplaceTextAutoformat);
                        }
                        else
                            Selection.Insert("\r\n" + prefix, (int)vsInsertFlags.vsInsertFlagsCollapseToEnd);

                    }
                    else
                    {
                        Debug.Assert(Keypress == "\r");
                        // Do the insert in three steps, so that the first ctrl-z undoes the comment chars
                        // then the indenting, then the newline
                        Selection.Insert("\r\n", (int)vsInsertFlags.vsInsertFlagsCollapseToEnd);
                        if (commentStartIdx != 0)
                            Selection.Insert(prefix.Substring(0, commentCharsIdx), (int)vsInsertFlags.vsInsertFlagsCollapseToEnd);
                        Selection.Insert(prefix.Substring(commentCharsIdx), (int)vsInsertFlags.vsInsertFlagsCollapseToEnd);
                    }
                }
            }
        }
    }

    /// <summary>
    /// Given a string and a start position, return the position of the first non-whitespace
    /// character.  Returns str.Length if there is no such character.  
    /// </summary>
    static private int SkipSpace(string str, int startPosition)
    {
        int i;
        for (i = startPosition; i < str.Length; i++)
        {
            Char c = str[i];
            if (c != ' ' && c != '\t')
                return i;
        }
        return i;
    }

    /// <summary>
    /// Is the given edit point that is JUST a comment (no code preceeds it). 
    /// Currently we only support // sytle comments.  
    /// </summary>
    static private bool IsCommentLine(EditPoint point)
    {
        Debug.Assert(point.AtStartOfLine);

        EditPoint endOfLine = point.CreateEditPoint();
        endOfLine.EndOfLine();
        string line = point.GetText(endOfLine);

        int idx = SkipSpace(line, 0);
        if (idx >= line.Length)
            return false;
        // TODO wierd corner cases trying to support both VB and C# comment syntaxes.  
        if (String.Compare(line, idx, "//", 0, 2) == 0 || String.Compare(line, idx, "'", 0, 1) == 0)
            return true;
        return false;
    }

    /// <summary> Conceptually, this routine is simple. It takes a string 'comment' and returns that
    /// string 'pretty printed' which means that you word wrapped what as in the comment to look nice.
    /// 
    /// The basic algorithm it uses is to look for a paragraph and then word-wrap the paragraph. A
    /// paragraph is defined as a set of non empty (thus a blank line breaks a paragraph), The
    /// indentation level of the paragraph is that of the first line.
    /// 
    /// To support outline (hierarchical bulleted lists), a simple convention is used. Lines that begin
    /// with a * are 'bullets' in the outline. They always start a paragraph (thus will never be wrapped
    /// with its predecessor line), and the wrapping is indented by 4 spaces.
    /// 
    /// To support another style of outlining, if a line ends in a ':' character then the next line will
    /// not be considered part of the same paragraph. This allows another common form of outline style to
    /// be supported.
    /// 
    /// Finally to support XML tags in comments, the less than sign at the beinging of a line will force
    /// a new paragraph (like a *) and a greater at then end of a line will also force a paragraph (like
    /// :).
    /// 
    /// These rule have a 'bug' in them in that if the comment text has any of special characters in them
    /// and they happen to be wrapped to the begining or the end of a line (where the have significance),
    /// then they will incorrectly inhibit word wrapping. This is why we keep the number of such special
    /// characters low.
    /// 
    /// Note that this routine does not require // style comments, it just tolerates them (treats the //
    /// like whitespace). Thus this routine can work on any text, not just text inside comments. It
    /// simply propages the prefix whitespace comment start characters. </summary>
    static private string FormatCommentString(string comment)
    {
        // remove tabs, break into lines.
        comment = comment.Replace("\t", "    ");
        string[] lines = comment.Split(new string[] { "\r\n" }, StringSplitOptions.None);

        // parse the line into prefixes (the comment start sequence and whitespace), and the data
        List<string> prefixes = new List<string>();
        List<string> dataLines = new List<string>();
        for (int i = 0; i < lines.Length; i++)
        {
            string line = lines[i];
            if (line.Length == 0 && i + 1 == lines.Length)  // Split creates a empty line at the end.  ignore it. 
                break;

            Match m = Regex.Match(line, @"^(\s*[/']*\s*)(.*?)\s*$");
            Debug.Assert(m.Success);
            prefixes.Add(m.Groups[1].Value);
            dataLines.Add(m.Groups[2].Value);
        }

        // Find 'paragraphs' (things that should wrap as a block)  These are delimited by blank lines or lines
        // whose data begin with *
        StringBuilder newComment = new StringBuilder();
        int outputColumn0 = 0;          // current column number

        string prefix = null;           // prefix == null means 'start a new paragraph.  
        for (int i = 0; i < dataLines.Count; i++)
        {
            string dataLine = dataLines[i];
            bool bulletLine = dataLine.StartsWith("*");
            bool bannerLine = BannerLine(dataLine);
            bool lineBreakNeeded = bulletLine || dataLine.StartsWith("<") || dataLine.StartsWith("#") || bannerLine;
            if (dataLine.Length == 0 || lineBreakNeeded)     // Should we complete the previous paragraph?
            {
                if (outputColumn0 > 0)
                    newComment.AppendLine();
                outputColumn0 = 0;
            }

            if (prefix == null || lineBreakNeeded)           // Are we starting a paragraph? get the prefix
                prefix = prefixes[i];

            if (dataLine.Length == 0)                        // we dont word wrap blank lines (they would disappear)
            {
                newComment.AppendLine(prefix);
                outputColumn0 = 0;
                prefix = null;
            }
            else
            {
                bool atLineStart = false;
                if (lineBreakNeeded)
                {
                    Debug.Assert(outputColumn0 == 0);   // set above.  
                    newComment.Append(prefix);
                    outputColumn0 += prefix.Length;
                    atLineStart = true;
                    if (bulletLine)
                        prefix += "    ";               // bulletLine wrap indented 4 more spaces;
                }
                WordWrapLine(newComment, ref outputColumn0, dataLine, prefix, atLineStart);

                // colon lines or XML tags end a paragraph.  
                if (dataLine.EndsWith(":") || dataLine.EndsWith(">") || bannerLine)
                {
                    newComment.AppendLine();
                    outputColumn0 = 0;
                    prefix = null;
                }
            }
        }

        if (outputColumn0 > 0)                      // End any incomplete lines
            newComment.AppendLine();

        return newComment.ToString();
    }

    /// <summary>
    /// We don't want to wrap banner lines (lines that don't have words on them). 
    /// </summary>
    static private bool BannerLine(string dataLine)
    {
        return !Regex.IsMatch(dataLine, @"[A-Za-z]");
    }

    /// <summary>
    /// Takes the words in 'dataLine' and writes them out into 'newComment', keeping the
    /// column number (outputColumn0) up to date. (The first column is column 0, not 1).  
    /// When we reach column 'wrapColumn' we will inject a newline.  Before any output
    /// to column 0, we output 'prefix' (which is text that comments and indents it properly). 
    /// </summary>
    static private void WordWrapLine(StringBuilder newComment, ref int outputColumn0, string dataLine, string prefix, bool atLineStart)
    {
        string[] words = dataLine.Split((char[])null, StringSplitOptions.RemoveEmptyEntries);        // Split on whitespace
        foreach (string word in words)
        {
            if (outputColumn0 != 0)
            {
                if (outputColumn0 + word.Length + 1 < wrapColumn) // word will fit on line
                {
                    if (!atLineStart)
                    {
                        newComment.Append(' ');
                        outputColumn0++;
                    }
                }
                else
                {
                    newComment.AppendLine();
                    outputColumn0 = 0;
                }
            }

            if (outputColumn0 == 0)
            {
                newComment.Append(prefix);
                outputColumn0 += prefix.Length;
            }
            newComment.Append(word);
            outputColumn0 += word.Length;
            atLineStart = false;
        }
    }

    static int wrapColumn;                                          // column to word wrap from 
    private EnvDTE80.TextDocumentKeyPressEvents textDocKeyEvents;   // The event that I have hooked into (so I can remove myself)
    private DTE2 dte;                                               // For talking to VS
    #endregion
}

#if Development 
    /// <summary>
    /// Implements commands that are useful for developing a new addin (getting names etc)
    /// </summary>
    class AddinDevelopment
    {
        public AddinDevelopment(DTE2 dte)
        {
            this.dte = dte;
        }

        public void PrintDiagnosticsCommand()
        {
            // We want to add options to the code window popup, fetch its commandBar. 
            CommandBars cmdBars = (CommandBars)dte.CommandBars;
            OutputWindowPane log = Log.LogPane;
            if (log == null)
                return;

            foreach (CommandBar myCmdBar in cmdBars)
            {
                log.OutputString("Got CommandBar named " + myCmdBar.Name + " visible " + myCmdBar.Visible + "\r\n");
                log.OutputString("    Type: " + myCmdBar.Type.ToString() + "\r\n");
                int children = myCmdBar.Controls.Count;
                if (children > 0)
                {
                    log.OutputString("    Children: " + myCmdBar.Controls.Count + "\r\n");
                    foreach (CommandBarControl child in myCmdBar.Controls)
                    {
                        log.OutputString("        Child type = " + child.Type + " caption " + child.Caption + "\r\n");
                    }
                }
            } 

            log.OutputString("****************************************************************\r\n");


            Commands2 commands = (Commands2)dte.Commands;
            foreach (Command cmd in dte.Commands)
            {
                string name = cmd.Name;
                string bindingsStr = "";
                object[] bindings = (object[])cmd.Bindings;
                foreach (string binding in bindings)
                    bindingsStr += " " + binding;

                log.OutputString("Got Command named " + name + " bindings " + bindingsStr + "\r\n");
            }

            log.OutputString("****************************************************************\r\n");

            Properties properties;
            properties = dte.get_Properties("Environment", "General");
            log.OutputString("General Options (count = " + properties.Count + ")\r\n");
            PrintProperties(log, properties);

            if (dte.Globals.get_VariableExists("MyData"))
            {
                string value = (string)dte.Globals["MyData"];
                log.OutputString("Got mydata = " + value + "\r\n");
            }
            dte.Globals["MyData"] = DateTime.Now.ToString();
            dte.Globals.set_VariablePersists("MyData", true);

            log.OutputString("****************************************************************\r\n");


            Solution sln = dte.Solution;
            log.OutputString("Properties for solution " + sln.FullName + "\r\n");
            PrintProperties(log, sln.Properties);

            log.OutputString("Projects\r\n");
            foreach (Project project in sln.Projects)
            {
                log.OutputString("Project " + project.Name + " Properties\r\n");
                PrintProperties(log, project.Properties);

                Globals projectGlobal = project.Globals;
                object[] names = (object[])projectGlobal.VariableNames;
                log.OutputString("Project " + project.Name + " has " + names.Length + " globals\r\n");

                log.OutputString("Project " + project.Name + " Items\r\n");
                foreach (ProjectItem projectItem in project.ProjectItems)
                {
                    log.OutputString("    item " + projectItem.Name + "\r\n");
                }
            }

            Log.WriteLine("Done");
        }

        private void PrintProperties(OutputWindowPane log, Properties props)
        {
            foreach (Property prop in props)
            {
                try
                {
                    if (prop.NumIndices == 0)
                        log.OutputString("    property " + prop.Name + " = " + prop.Value + "\r\n");
                    else
                        log.OutputString("    property " + prop.Name + " has " + prop.NumIndices + " indexes\r\n");
                }
                catch
                {
                    log.OutputString("Exception happened during processing of propery " + prop.Name + "\r\n");
                }
            }

        }

#region privates
        DTE2 dte;
#endregion
    }
#endif

